/*******************************************************************************
 * Copyright（C） CETC-32
 * @file:ServiceAuthInerceptors.java
 * @Description：
 * @Author ：徐文远
 * @version：1.0
 * @date ： 2021/2/1 下午4:29
 ******************************************************************************/

package com.cetc32.webutil.client.interceptors;

import com.alibaba.fastjson.JSON;
import com.cetc32.webutil.client.bean.LoginStatus;
import com.cetc32.webutil.client.bean.SSOProperties;
import com.cetc32.webutil.common.annotations.AccessPermission;
import com.cetc32.webutil.common.annotations.LoginRequired;
import com.cetc32.webutil.common.annotations.LoginSkipped;
import com.cetc32.webutil.common.bean.LoginUser;
import com.cetc32.webutil.client.bean.URLPermissions;
import com.cetc32.webutil.common.util.CookieUtil;
import com.cetc32.webutil.common.util.HttpclientUtil;
import com.cetc32.webutil.common.util.JWTUtil;
import com.cetc32.webutil.common.util.SecurityUserUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.mvc.ParameterizableViewController;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Map;
/****
 * SSO 登陆拦截器
 * 拦截请求并校验请求是否应该被放行
 * 原理说明：
 * 首先默认跳转到认证中心认证中心根据认证中心的cookie中token校验是否成功，
 * 成功则直接返回到元地址并在url传递参数为token的url请求
 * 此时Interceptor将拦截到token并保存到cookie
 *
 * ***/
@Component
public class ServiceAuthInerceptors extends HandlerInterceptorAdapter {

    @Autowired
    SSOProperties sso;
    @Value("${sso.syscode}")
    private String sysCode;
    @Value("${sso.tokenAge:-1}")
    private  Integer tokenAge;

    Logger logger = LoggerFactory.getLogger(ServiceAuthInerceptors.class);
    /**
     * 校验tokenUrl
     * */
   /* @Value("${verifyUrl}")
    String verifyUrl;*/
    /**
     * SSO 登陆页面Url
     * */
   /* @Value("${passportUrl}")
    String passportUrl;*/

    /*@Value("${defaultLoginRequiredEnable}")
    boolean defaultLoginRequiredEnable;*/

    @Autowired
    URLPermissions urlPermissions;
    /**
     * 监测接口是否为API
     * 如果是web service接口：返回权限校验失败信息时候，交由前端去跳转到指定路径
     * 如果是视图接口：返回权限校验失败信息时候，直接跳转到指定路径
     * **/
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        boolean isApi=false;
        /**
         * 针对资源类的请求直接放行
         * */
        if(handler instanceof ResourceHttpRequestHandler){
            return true;
        }
        if(handler instanceof ParameterizableViewController){
            ParameterizableViewController pvc = (ParameterizableViewController)handler;
            logger.debug("ParameterizableViewController:  {}",pvc.getViewName());

            return true;
        }
        //判断被拦截的请求的访问的方法的注解（是否是需要拦截的）
        String uri = request.getRequestURI().replace(request.getContextPath(),"");
        if(sso.getVerifyUrl().contains(request.getRequestURI()))
            return true;
        LoginStatus v =isCurrentUsrLogin(request, response, (HandlerMethod) handler);
        HandlerMethod hm = (HandlerMethod) handler;
        //hm.getMethod().getReturnType()
        /**
         * xxx.xxxx.xxx.controller;
         * **/
        //System.out.println("ReturnType : "+hm.getReturnType().getDeclaringClass().getCanonicalName());
        //System.out.println("Method Return Type0 : "+hm.getMethod().getName());
        //System.out.println("Method Return Type1 : "+hm.getMethod().getDeclaringClass().getMethod(hm.getMethod().getName()).getReturnType().getCanonicalName());
        //System.out.println("Method Return Type2 : "+hm.getMethod().getAnnotatedReturnType().getClass().getCanonicalName());
        //java.lang.class
        /*System.out.println("Method Return Type3 : "+hm.getMethod().getGenericReturnType().getClass().getCanonicalName());
        System.out.println("Method Return Type4 : "+hm.getMethod().getReturnType().getClass().getCanonicalName());
        */
        isApi = isApi(hm,request,response);
        logger.debug(" {} LoginStatus:{} isApi: {}",uri, v.value() ,isApi);

        if(isUrlLoginSkipped(hm, uri))
            return true;
        logger.debug(" {} LoginStatus:{}",uri, v.value() );
        if(LoginStatus.LOGINSUCCESS == v){
            /**
             * 没有指定访问权限的则默认允许访问，否则检验是会被认为没有访问权限
             * **/
            AccessPermission classAcess =hm.getMethod().getDeclaringClass().getAnnotation(AccessPermission.class);
            AccessPermission methodAcess =hm.getMethodAnnotation(AccessPermission.class);
            LoginUser loginUser = (LoginUser) request.getSession().getAttribute("loginUser");


            boolean hasPermission=true;
            if(methodAcess != null ){
                hasPermission= loginUser.hasPermision(methodAcess.permissions())||loginUser.hasPermision(methodAcess.permission());

            }else if(classAcess != null){
                hasPermission =loginUser.hasPermision(classAcess.permission()) || loginUser.hasPermision(classAcess.permissions());
            }else {
                hasPermission= urlPermissions.hasPermission(loginUser,uri);
            }

            if(!hasPermission){
                if(isApi){
                    response.getWriter().print("{ \"code\":403,\"message\":\"Permission is not found！\"}");
                }else{
                    request.getSession().invalidate();
                    CookieUtil.setCookie(request,response,"token","",0,false);
                    response.sendRedirect(sso.getPassportUrl()+request.getRequestURL()+"&cate=0");
                }
                return false;
            }
            return true;
        }else if(LoginStatus.UNLOGIN == v){
            if(isApi)
                response.getWriter().print("{ \"code\":401,\"message\":\"未登陆用户，请先登陆\",\"url\":\""+sso.getPassportUrl()+"\"}");
            else
                response.sendRedirect(sso.getPassportUrl()+request.getRequestURL());
            return false;
        }else{
            return true;
        }
    }
    /***
     * 校验等前用户时候是已登陆用户
     * **/
    private LoginStatus isCurrentUsrLogin(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler) throws IOException {
        HandlerMethod hm = handler;

        /**
         * 如果session空且session中存在loginUser属性
         * 则直接放行请求
         * **/
        if(null != request.getSession(false) && null != request.getSession().getAttribute("loginUser")){
            SecurityUserUtil.setUser((LoginUser)request.getSession().getAttribute("loginUser"),sysCode);
            return LoginStatus.LOGINSUCCESS;
        }
        /**
         * 2021.5.10调整此处代码到getSession之后为了让session有效期内能正常调用接口
         * 获取token，先从url param中获取
         * 如果没有则从cookies中获取
         * 最后如果仍然找不到就从header中获取
         * （从Header中获取token的情况主要针对跨域的api请求，原始域中有token信息，可以将其封装到header中）
         */
        String token = "",oldToken=CookieUtil.getCookieValue(request, "token", true);
        /*token = request.getParameter("token");
        logger.debug("Url Token: {}",token );*/
        /*if(StringUtils.isBlank(token)) {
            token = CookieUtil.getCookieValue(request, "token", true);
            oldToken =token;
        }
        if(StringUtils.isBlank(token)){
            token=request.getHeader("token");
        }*/
        token = getFinalAvaliableToken(request);
        logger.debug("Final Token: {}" ,token );
        /**
         * 如果token为空或者token已经过期则告知用户需要重新登陆
         * **/
        if(null == token) {//token过期
            logger.debug("Token Expire 0: {} ",JWTUtil.getExpire(token)  );
            //logger.debug("Token Expire 1: {} ",System.currentTimeMillis()/1000  );
            return LoginStatus.UNLOGIN;
        }

        /***
         * session中未曾找到用户信息，则认为用户为登陆，需要重token，cookie等位置查找时候有用户信息
         * **/
        String success = "fail";
        String successJSON = HttpclientUtil.doGet(sso.getVerifyUrl()+token);
        logger.debug("Send Http : {}",sso.getVerifyUrl()+token);
        Map successMap = JSON.parseObject(successJSON, Map.class);
        logger.debug("successMap : {}",successMap.toString());
        if(successMap.get("message") != null){
            success = successMap.get("message").toString();
            logger.debug("token verify: {}",success);
        }
        /**
         * 如果校验token成功则直接写入session信息
         * 否则校验是否有LoginRequired注解
         * 如果LoginRequired注解loginSucess = false,则可以放行
         * 否则需要用户登陆
         * */
        if(success.equals("success")){
            /**
             * 获取到token则校验cookies中token是否失效，失效则重新写入cookies
             * **/
            if(StringUtils.isNotBlank(token) && !token.equals(oldToken)){
                CookieUtil.setCookie(request,response,"token",token,tokenAge,true);
            }
            LoginUser loginUser =JSON.parseObject(successMap.get("data").toString(),LoginUser.class);
            request.getSession().setAttribute("loginUser",loginUser);
            request.setAttribute("loginUser",loginUser);
            SecurityUserUtil.setUser(loginUser,sysCode);
            CookieUtil.setCookie(request,response,"AuthorizationUserId",loginUser.getUsername(),tokenAge,true);
            return LoginStatus.LOGINSUCCESS;
        }else{
            logger.debug("verify Failed {}" ,token);
            return LoginStatus.UNLOGIN;
        }
    }

    private String getFinalAvaliableToken(HttpServletRequest request) {
        String headerToken =request.getHeader("token"),
                cookieTooken= CookieUtil.getCookieValue(request, "token", true);
        String urlToken =request.getParameter("token");
        logger.debug("urlToken: {}",urlToken);
        logger.debug("cookieTooken: {}",cookieTooken);
        logger.debug("headerToken: {}",headerToken);
        /*if(StringUtils.isNotBlank(urlToken)
                && StringUtils.isNotBlank(cookieTooken)
                && StringUtils.isNotBlank(headerToken) ){*/
           /* if(JWTUtil.verify(urlToken)
                    && JWTUtil.verify(cookieTooken)
                    && JWTUtil.verify(headerToken)){*/
        if(JWTUtil.verify(cookieTooken)
                || JWTUtil.verify(headerToken)
                || JWTUtil.verify(urlToken) ){
            long urlE=JWTUtil.getExpire(urlToken);
            long headE=JWTUtil.getExpire(headerToken);
            long cookiE=JWTUtil.getExpire(cookieTooken);
            logger.debug("urlToken     Expiration : {}",urlE);
            logger.debug("headerToken  Expiration : {}",headE);
            logger.debug("cookieTooken Expiration : {}",cookiE);
            if(urlE>= cookiE && urlE>= headE){
                return urlToken;
            }else if(cookiE>= urlE && cookiE>=headE){
                return cookieTooken;
            }else if(headE > 0){
                return headerToken;
            }
        }

            /*}else{
                if(JWTUtil.verify(urlToken)){
                    return urlToken;
                }
                if(JWTUtil.verify(cookieTooken)){
                    return cookieTooken;
                }
            }*/
        /*}else{
            if(StringUtils.isNotBlank(urlToken) && JWTUtil.verify(urlToken)){
                return urlToken;
            }
            if(StringUtils.isNotBlank(cookieTooken) && JWTUtil.verify(cookieTooken)){
                return cookieTooken;
            }
        }*/
        return null;
    }

    /**
     *  是否拦截
     *  方法上有LoginSkipped时直接放行
     *  类上有LoginSkipped，方法上没有LoginRequired时也放行
     *  类/方法上都没有LoginRequiredq且defaultLoginRequiredEnable=false也放行
     *  如果是通过配置的方式，则校验是否启用默认登陆校验，启用的话需要校验是否有LoginSkipped，为启用的话要确认url为非LoginRequired
     *  classLogin  类上的loginRequired注解
     *  classSkipped 类上的loginSkipped注解
     *  methodLogin 方法上的loginRequired注解
     *  methodSkipped 方法上的loginSkipped注解
     * @param uri 当前URL去除ip及cotexpath后的路径
     * @return 返回是接口否为不需要登陆，true：不需要登陆，false：需要登陆
     * */
    private boolean isUrlLoginSkipped(HandlerMethod hm,  String uri) {
        LoginSkipped methodSkipped = hm.getMethodAnnotation(LoginSkipped.class);
        LoginSkipped classSkipped  = hm.getMethod().getDeclaringClass().getAnnotation(LoginSkipped.class);
        LoginRequired methodLogin =hm.getMethodAnnotation(LoginRequired.class);
        LoginRequired classLogin  = hm.getMethod().getDeclaringClass().getAnnotation(LoginRequired.class);
        if(methodSkipped != null )
            return true;
        if(classSkipped != null
                && methodLogin == null)
            return true;
        /**
         * 校验LoginRequired注解的loginSuccess属性是否为false
         * false则表示该请求可以允许不登陆就能进行访问，则放行不再强制要求用户登陆
         * */
        if(methodLogin != null && methodLogin.loginSuccess()==false){
            return true;
        }
        if(classLogin !=null && classLogin.loginSuccess() == false){
            if(methodLogin !=null && methodLogin.loginSuccess()== true )
                return false;
            return true;
        }
        if (classLogin  == null
                && methodLogin == null){
            if(sso.getDefaultLoginRequiredEnable() == false && false == sso.isLoginRequired(uri)){
                 return true;
            } //更新日期20210508
            if(sso.getDefaultLoginRequiredEnable()  && sso.isLoginSkipped(uri)){
                return true;
            }
        }
        return false;
    }

    /**
     * ####更新于2021.3.25
     * 新增基于http请求header中的accept识别请求类型是否为接口
     * ####更新于2021.3.5
     * 基于SpringMVC 校验，此方法决定了，当前SSO-client仅支持基于SpringMVC注解的Web工程
     * 检验该请求为视图请求还是接口请求，若为视图请求返回false，否则返回true
     * 校验条件：Controller注解与RestController注解鉴别
     * Controller为页面请求，若Controller注解类中的方法添加ResponseBody注解表示为接口请求
     * RestController注解的类一律认为是接口请求
     * @param hm HandlerMethod
     * @return false：视图请求返回内容为html页面，true：返回内容为数据
     * **/
    private boolean isApi( HandlerMethod hm,HttpServletRequest request,HttpServletResponse response) {

        Controller c =hm.getMethod().getDeclaringClass().getAnnotation(Controller.class);
        String accept= request.getHeader("accept");
        try {
            if(hm.getMethod().getDeclaringClass().getMethod(hm.getMethod().getName(),hm.getMethod().getParameterTypes()).getReturnType().getCanonicalName().equals(ModelAndView.class.getCanonicalName()) ){
                return false;
            }
        } catch (NoSuchMethodException e) {

        }
        if(null!=c){//监测该方法是否是Controller视图方法
            ResponseBody rb = hm.getMethodAnnotation(ResponseBody.class);
            //更新日期2021.3.25
            ResponseBody rbC = hm.getMethod().getDeclaringClass().getAnnotation(ResponseBody.class);
            if(rb!=null || rbC != null){//视图方法是否添加
                return true;
            }
        }else{
            RestController rc =hm.getMethod().getDeclaringClass().getAnnotation(RestController.class);

            if(rc!=null){
                return true;
            }
        }
        //2021.12.1注释掉，最终返回改为 false
        //2021.4.15调整到末尾
        //更新日期2021.3.5 校验基于header中accept识别接口还是页面
        /*if(accept.contains("text/html") || accept.contains("application/xhtml+xml"))
            return false;*/
        return false;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
       /* System.out.println("contentType="+response.getContentType());
        System.out.println("contentType="+request.getContentType());*/
    }
}
